#include "ThreadSimpleDemoOfC.h"
// 编译时错误：undefined reference to `pthread_create'。
// 这个错误是因为多线程操作并非语言能力，而是来自操作系统的支持，所以不同操作系统有不同的实现，并且以库文件形式提供。所以这里编译需要连接对应库文件(pthread 是掐头去尾的文件名)：
// 1. 可以在编译时通过编译命令附带，gcc -lpthread 或者 g++ -lpthread 来完成。
// 2. 可以通过 cmake 脚本中添加 link_libraries(pthread) 来支持。
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <stdio.h>

using namespace std;


// 线程启动函数：线程函数与规定的函数指针类型一致：void *(*start_routine) (void *)
// 注意签名格式，返回值和参数都是 void * 且都只有一个
void* subThreadAction(void *) 
{
    cout << "子线程的ID：" << pthread_self() << endl;

    for(int index = 0; index < 10; ++index) {
        if(index == 6)
        {
            // NULL 是 C 语言中的空指针宏，它是一个值为 0 的宏。nullptr 是 C++ 标准下的空指针对象，它是一个对象代表null。NULL 是一个宏所以不会进行类型检查校验，所以推荐使用nullptr
            pthread_exit(NULL);	// 直接退出子线程
        }
        cout << "子线程正在执行计数" << index << endl;
        // sleep(1);
    }
    return nullptr;
}

// 定义结构
struct Persion
{
    int id;
    char name[36];
    int age;
};
struct Persion globalPer;	// 定义全局变量

void threadSimpleDemoOfC()
{
    // 每一个线程都有一个唯一的线程 ID，ID 类型为 pthread_t，这个 ID 是一个无符号长整形数，如果想要得到当前线程的线程 ID，可以调用如下函数：
    // 返回当前线程的线程ID
    pthread_t tid = pthread_self();
    cout << "当前主线程的ID：" << tid << endl;

    // 在一个进程中调用线程创建函数，就可得到一个子线程，和进程不同，需要给每一个创建出的线程指定一个处理函数，否则这个线程无法工作。
    pthread_t subTid;
    // pthread_create 参数:
    //  * thread: 第一个参数，传出参数，是无符号长整形数，线程创建成功, 会将线程 ID 写入到这个指针指向的内存中；
    //  * attr: 第二个参数，线程的属性, 一般情况下使用默认属性即可, 写 NULL；
    //  * start_routine: 第三个参数，函数指针，创建出的子线程的处理动作，也就是该函数在子线程中执行；
    //  * arg: 第四个参数，作为实参传递到 start_routine 指针指向的函数内部；
    //  * 返回值：线程创建成功返回 0，创建失败返回对应的错误号。
    pthread_create(&subTid, nullptr, subThreadAction, nullptr);
    cout << "创建的子线程的ID：" << subTid << endl;

    for(int index = 0; index < 2; ++index) {
        cout << "主线程正在执行计数" << index << endl;
        // sleep(1);
    }

    // 休息一下
    // sleep(1);    // 会引发上下文切换。

    // 1. 创建线程并在执行完毕后退出 APP，这里为了模拟直接退出调用 exit(0) 来强行退出。
    // 在打印的日志输出中为什么子线程处理函数没有执行完毕呢（只看到了子线程的部分日志输出）？
    // 主线程一直在运行, 执行期间创建出了子线程，说明主线程有 CPU 时间片, 在这个时间片内将代码执行完毕了, 主线程就退出了。
    // 子线程被创建出来之后需要抢 cpu 时间片, 抢不到就不能运行，如果主线程退出了, 虚拟地址空间就被释放了, 子线程就一并被销毁了。
    // 但如果是某一个子线程退出了, 主线程仍会在运行, 
    // 上述情况在单核 CPU 百分百出现，但多核 CPU 并不一定，因为每个核都有一个时间片，一个进程虽然只能抢一个 CPU 时间片，但是多核之下每个核都可以抢一次。
    // 其实也容易想来，CPU 时间片只能运行一个线程代码，所以每个进程即便存在多个线程同一时间也只会有一个线程在执行着意味着也只会有一个进程在执行，但多个内核意味每个核都可以去执行一个线程
    // 得到的结论：在没有人为干预的情况下，虚拟地址空间的生命周期和主线程是一样的，与子线程无关。
    // 目前的解决方案: 让子线程执行完毕, 主线程再退出, 可以在主线程中添加挂起函数 sleep();
    // exit(0); // 推出进程，所有线程都会被停止
    

    // 2. 线程退出
    // 在编写多线程程序的时候，如果想要让线程退出，但是不会导致虚拟地址空间的释放（针对于主线程），我们就可以调用线程库中的线程退出函数，
    // 只要调用该函数当前线程就马上退出了，并且不会影响到其他线程的正常运行，不管是在子线程或者主线程中都可以使用。
    // 
    // 主线程调用退出函数退出, 地址空间不会被释放:
    //  * 参数: 线程退出的时候携带的数据，当前子线程的主线程会得到该数据。如果不需要使用，指定为NULL
    // 下面是线程退出的示例代码，可以在任意线程的需要的位置调用该函数：
    // pthread_exit(nullptr); // 主线程调用退出函数退出, 地址空间不会被释放

    
    // 3. 回收线程资源
    // 线程和进程一样，子线程退出的时候其内核资源主要由主线程回收，线程库中提供的线程回收函数叫做 pthread_join()，
    // 这个函数是一个阻塞函数，如果还有子线程在运行，调用该函数就会阻塞，子线程退出函数解除阻塞进行资源的回收，函数被调用一次，只能回收一个子线程，如果有多个子线程则需要循环进行回收。
    //
    // summer：这里我的理解是，pthread_join 必须要再主线程进行回收，因为主线程回收的是内核资源，系统资源。所以我们不能再别的线程去回收。限定线程的条件下，
    //         此时 pthread_join 还是个阻塞函数。那么显然如果我开始了一个线程的回收，然后阻塞等待，那么其他线程自然就不能被回收了，因为执行不到哪里去。
    // summer: 另外关于线程返回值，虽然函数指针的结构是 void* (*)(void*) 存在一个指针返回值，但是调用这个函数指针的其实是开辟的线程，所以正常通过函数返回的数据我们是拿不到的，
    //         所以，需要使用一些特殊方式来获取，比如 pthread_exit 来传出，pthread_join 来接受。然而 pthread_join 只能再主线程中回收，那么返回值其实也只能由主线程来获取。
    // summer: 补充，其实不通过 pthread_exit 通过 return 在线程返回值返回也是可以被 pthread_join 捕获到的。究其原因其实应该是创建线程得时候虽然是回调了指针函数，
    //         但是在函数返回后其实线程控制应该也能执行类似 pthread_exit 函数得行为。
    // 
    // 这是一个阻塞函数, 如果子线程还在运行，那么这个函数就会阻塞调用的线程，直到子线程退出, 函数就解除阻塞并回收对应的子线程资源, 类似于回收进程使用的函数 wait()。
    // 参数:
    //  * thread: 要被回收的子线程的线程ID
    //  * retval: 二级指针, 指向一级指针的地址, 是一个传出参数, 这个地址中存储了pthread_exit() 传递出的数据，如果不需要这个参数，可以指定为NULL
    //  * 返回值：线程回收成功返回 0，回收失败返回错误号。
    // pthread_join(subTid, nullptr);


    // 4. 回收子线程资源并获取返回数据
    // 在子线程退出的时候可以使用 pthread_exit() 的参数将数据传出，在回收这个子线程的时候可以通过 phread_join() 的第二个参数来接收子线程传递出的数据。
    // 接收数据有很多种处理方式，下面来列举几种：
    // 
    // 通过函数 pthread_exit(void *retval); 可以得知，子线程退出的时候，需要将数据记录到一块内存中，通过参数传出的是存储数据的内存的地址，而不是具体数据，
    // 由因为参数是 void* 类型，所有这个万能指针可以指向任意类型的内存地址。
    /** 示例代码需要查看可以删除多行注释
    pthread_t ret_case_tid;
    struct Persion outPer;
    pthread_create(&ret_case_tid, nullptr, [](void* data) -> void* {
        struct Persion* callThreadPer = (struct Persion*) data;
        cout << "带返回结果的子线程的 ID：" << pthread_self() << endl;
        for(int i=0; i<9; ++i)
        {  
            cout << "child == i:" << i << endl;
            if(i == 6)
            {
                // 一、使用子线程栈
                // 先来看第一种方式，将子线程退出数据保存在子线程自己的栈区：
                struct Persion p;
                // 四、使用堆内存指针
                // struct Persion* pPer = new Persion;
                p.age = 12;
                // pPer->age = 12;
                // 二、使用全局变量
                // 位于同一虚拟地址空间中的线程，虽然不能共享栈区数据，但是可以共享全局数据区和堆区数据，因此在子线程退出的时候可以将传出数据存储到全局变量、静态变量或者堆内存中。
                globalPer.age = 12;
                // 三、使用主线程栈
                // 虽然每个线程都有属于自己的栈区空间，但是位于同一个地址空间的多个线程是可以相互访问对方的栈空间上的数据的。
                // 由于很多情况下还需要在主线程中回收子线程资源，所以主线程一般都是最后退出，基于这个原因在下面的程序中将子线程返回的数据保存到了主线程的栈区内存中：
                callThreadPer->age = 12;
                strcpy(p.name, "tom");
                // strcpy(pPer->name, "tom");
                strcpy(globalPer.name, "tom");
                strcpy(callThreadPer->name, "tom");
                p.id = 100;
                // pPer->id = 100;
                globalPer.id = 100;
                callThreadPer->id = 100;
                // 该函数的参数将这个地址传递给了主线程的 pthread_join()
                // pthread_exit(&p);            // 局部变量会随着线程释放而被释放，使用下面的全局变量或指针可以解决
                // pthread_exit(&globalPer);    // 全局变量可以用于传输，并不会随着子线程释放而释放，因为全局变量的声明周期时整个APP
                // 实际上下面的调用中使用了通过 pthread_create 传递进来的变量 outPer 的地址，其实传出的指针变量和 outPer 是一个内存区块。
                pthread_exit(callThreadPer);    // 使用调用线程传入的变量用于传输，调用线程传入的变量的声明周期是在调用线程内的生命周期，也不会随着子线程释放而释放
                // 这种方式可能并不好维护，不是很推荐，因为指针是在局部创建，但是使用却是在外部使用，那么谁来管理和释放这个指针就变的非常麻烦。
                // pthread_exit(pPer);    // 使用堆内存指针，虽然指针对象是在局部子线程创建，但是其对象是在堆上不会随着局部子线程的释放而释放。
            }
        }
        return nullptr; // 代码执行不到这个位置就退出了
    }, &outPer);
    cout << "创建带返回结果的子线程的 ID：" << ret_case_tid << endl;

    // ptr 是一个传出参数, 在函数内部让这个指针指向一块有效内存。这个内存地址就是 pthread_exit() 参数指向的内存
    void* pret = nullptr;
    // 阻塞等待子线程退出并写入返回数据到 pret
    pthread_join(ret_case_tid, &pret);

    // 这里出现问题了，当我们使用 struct Persion p; 局部变量作为回传数据时，并没有打印出回传参数，甚至引发异常导致程序退出了。原因如下：
    //   如果多个线程共用同一个虚拟地址空间，每个线程在栈区都有一块属于自己的内存，相当于栈区被这几个线程平分了，
    //   当线程退出，线程在栈区的内存也就被回收了，因此随着子线程的退出，写入到栈区的数据也就被释放了。
    //   所以 struct Persion p; 所定义的对象并非指针也非全局变量，而是在栈上的局部变量，当线程退出对象也被释放。所返回的指针指向内存区块会变成无效区块。
    //   即，void* pret 指向了无效区块，自然取不到数据并且还会因为使用引发异常导致程序崩溃。
    struct Persion* pp = (struct Persion*) pret;
    cout << "带返回结果的子线程的结果数据：name->" << pp->name << "; age->" << pp->age << "; id->" << pp->id << endl;
    cout << "带返回结果的子线程的结果数据：name->" << outPer.name << "; age->" << outPer.age << "; id->" << outPer.id << endl;
    cout << "子线程资源被成功回收...\n";
    */


    // 5. 线程分离
    // 在某些情况下，程序中的主线程有属于自己的业务处理流程，如果让主线程负责子线程的资源回收，调用 pthread_join() 只要子线程不退出主线程就会一直被阻塞，主要线程的任务也就不能被执行了。
    // 在线程库函数中为我们提供了线程分离函数 pthread_detach()，调用这个函数之后指定的子线程就可以和主线程分离，当子线程退出的时候，其占用的内核资源就被系统的其他进程接管并回收了。
    // 线程分离之后在主线程中使用 pthread_join() 就回收不到子线程资源了。
    
    // 设置子线程和主线程分离
    pthread_detach(subTid);
    // 如果是 pthread_exit 之后在 exit 那么分离的线程并不会受到进程结束退出的影响，但是如果没有 pthread_exit 直接退出，即便分离了的线程也会被停止。
    // 所以，pthread_detach 分离只会让线程脱离主线程的不需要再主线程中回收，而是自己回收自己，但是依然受到主线程牵连。
    // pthread_exit(NULL);
    // exit(0);
    // // 让主线程自己退出即可
    // pthread_exit(NULL);


    // 6. 其他线程函数
    // 6.1. 线程取消
    //  线程取消的意思就是在某些特定情况下在一个线程中杀死另一个线程。使用这个函数杀死一个线程需要分两步：
    //   * 在线程 A 中调用线程取消函数 pthread_cancel，指定杀死线程 B，这时候线程 B 是死不了的
    //   * 在线程 B 中进行一次系统调用（从用户区切换到内核区），否则线程 B 可以一直运行。
    //  这其实和七步断肠散、含笑半步癫的功效是一样的，吃了毒药不动或者不笑也没啥事儿。
    /*
    pthread_t cancel_tid;
    pthread_create(&cancel_tid, nullptr, [](void*) -> void* {
        int j = 0;
        for(int i = 0; i < 9; ++i)
        {
            j++;
        }
        // 这个函数会调用系统函数, 因此这是个间接的系统调用
        // 这个在 windows 下似乎不能触发
        printf("我是子线程, 线程ID: %ld\n", pthread_self());
        for(int i = 0; i < 100; ++i)
        {
            printf(" cancel child i: %d\n", i);
        }
        return NULL;
    }, nullptr);

    // 杀死子线程, 如果子线程中做系统调用, 子线程就结束了
    // 参数：要杀死的线程的线程ID
    // 返回值：函数调用成功返回0，调用失败返回非0错误号。
    pthread_cancel(cancel_tid);
    // 让主线程自己退出即可
    pthread_exit(NULL);
    */
    
    // 关于系统调用有两种方式：
    //  * 直接调用 Linux 系统函数
    //  * 调用标准 C 库函数，为了实现某些功能，在 Linux 平台下标准 C 
    // 在任何时候，都应该设计一种让线程自动执行完成并结束线程的机制，而非强制终止，因为那样会导致一些流程未执行玩，
    // 如果在windows下pthread_cancel还会失效如果使用windows的强制终止甚至可能会导致析构函数无法调用从而造成内存泄漏。


    // 6.2 线程ID比较
    // 在 Linux 中线程 ID 本质就是一个无符号长整形，因此可以直接使用比较操作符比较两个线程的 ID，但是线程库是可以跨平台使用的，在某些平台上 pthread_t 可能不是一个单纯的整形，
    // 这中情况下比较两个线程的 ID 必须要使用比较函数，函数原型如下：
    // 参数：t1 和 t2 是要比较的线程的线程 ID
    // 返回值：如果两个线程 ID 相等返回非 0 值，如果不相等返回 0
    // int pthread_equal(pthread_t t1, pthread_t t2);
    if(pthread_equal(tid, subTid)) {
        cout << "tid, subTid 相等" << endl;
    } else {
        cout << "tid, subTid 不相等" << endl;
    }
    if(pthread_equal(tid, pthread_self())) {
        cout << "当前在主线程中" << endl;
    } else {
        cout << "当前不在主线程中" << endl;
    }




    // 在处理多任务程序的时候使用多线程比使用多进程要更有优势，但是线程并不是越多越好，如何控制线程的个数呢？
    //  * 文件IO操作：文件IO对CPU是使用率不高, 因此可以分时复用CPU时间片, 线程的个数 = 2 * CPU核心数 (效率最高)
    //  * 处理复杂的算法(主要是CPU进行运算, 压力大)，线程的个数 = CPU的核心数 (效率最高)
}